diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt
index cd9f38f4cb..020124b648 100644
--- a/common/CMakeLists.txt
+++ b/common/CMakeLists.txt
@@ -280,6 +280,7 @@ set( COMMON_SRCS
     ${COMMON_PREVIEW_ITEMS_SRCS}
     ${PLOTTERS_CONTROL_SRCS}
     advanced_config.cpp
+    array_axis.cpp
     array_options.cpp
     base_struct.cpp
     bezier_curves.cpp
diff --git a/common/array_axis.cpp b/common/array_axis.cpp
new file mode 100644
index 0000000000..8737f7b62c
--- /dev/null
+++ b/common/array_axis.cpp
@@ -0,0 +1,153 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#include <array_axis.h>
+
+
+/**
+ * @return False for schemes like 0,1...9,10
+ *         True for schemes like A,B..Z,AA (where the tens column starts with char 0)
+ */
+static bool schemeNonUnitColsStartAt0( ARRAY_AXIS::NUMBERING_TYPE type )
+{
+    return type == ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL
+           || type == ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_NO_IOSQXZ;
+}
+
+
+ARRAY_AXIS::ARRAY_AXIS() : m_type( NUMBERING_TYPE::NUMBERING_NUMERIC ), m_offset( 0 )
+{
+}
+
+
+const wxString& ARRAY_AXIS::GetAlphabet() const
+{
+    static const wxString alphaNumeric = "0123456789";
+    static const wxString alphaHex = "0123456789ABCDEF";
+    static const wxString alphaFull = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    static const wxString alphaNoIOSQXZ = "ABCDEFGHJKLMNPRTUVWY";
+
+    switch( m_type )
+    {
+    default:
+    case NUMBERING_NUMERIC:
+        return alphaNumeric;
+    case NUMBERING_HEX:
+        return alphaHex;
+    case NUMBERING_ALPHA_NO_IOSQXZ:
+        return alphaNoIOSQXZ;
+    case NUMBERING_ALPHA_FULL:
+        return alphaFull;
+    }
+}
+
+
+OPT<int> ARRAY_AXIS::getNumberingOffset( const wxString& str ) const
+{
+    if( str.length() == 0 )
+        return OPT<int>{};
+
+    const wxString& alphabet = GetAlphabet();
+
+    int       offset = 0;
+    const int radix = alphabet.length();
+
+    for( unsigned i = 0; i < str.length(); i++ )
+    {
+        int chIndex = alphabet.Find( str[i], false );
+
+        if( chIndex == wxNOT_FOUND )
+            return OPT<int>{};
+
+        const bool start0 = schemeNonUnitColsStartAt0( m_type );
+
+        // eg "AA" is actually index 27, not 26
+        if( start0 && i < str.length() - 1 )
+            chIndex++;
+
+        offset *= radix;
+        offset += chIndex;
+    }
+
+    return OPT<int>{ offset };
+}
+
+
+void ARRAY_AXIS::SetAxisType( NUMBERING_TYPE aType )
+{
+    m_type = aType;
+}
+
+
+bool ARRAY_AXIS::SetOffset( const wxString& aOffsetName )
+{
+    OPT<int> offset = getNumberingOffset( aOffsetName );
+
+    // The string does not decode to a valid offset
+    if( !offset )
+        return false;
+
+    SetOffset( *offset );
+    return true;
+}
+
+
+void ARRAY_AXIS::SetOffset( int aOffset )
+{
+    m_offset = aOffset;
+}
+
+
+int ARRAY_AXIS::GetOffset() const
+{
+    return m_offset;
+}
+
+
+wxString ARRAY_AXIS::GetItemNumber( int n ) const
+{
+    wxString        itemNum;
+    const wxString& alphabet = GetAlphabet();
+
+    const bool nonUnitColsStartAt0 = schemeNonUnitColsStartAt0( m_type );
+
+    bool firstRound = true;
+    int  radix = alphabet.Length();
+
+    n += m_offset;
+
+    do
+    {
+        int modN = n % radix;
+
+        if( nonUnitColsStartAt0 && !firstRound )
+            modN--; // Start the "tens/hundreds/etc column" at "Ax", not "Bx"
+
+        itemNum.insert( 0, 1, alphabet[modN] );
+
+        n /= radix;
+        firstRound = false;
+    } while( n );
+
+    return itemNum;
+}
\ No newline at end of file
diff --git a/common/array_options.cpp b/common/array_options.cpp
index 8a2faa4e84..d2e9dd605d 100644
--- a/common/array_options.cpp
+++ b/common/array_options.cpp
@@ -25,86 +25,6 @@
 
 #include <trigo.h>
 
-const wxString& ARRAY_OPTIONS::AlphabetFromNumberingScheme( NUMBERING_TYPE_T type )
-{
-    static const wxString alphaNumeric = "0123456789";
-    static const wxString alphaHex = "0123456789ABCDEF";
-    static const wxString alphaFull = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
-    static const wxString alphaNoIOSQXZ = "ABCDEFGHJKLMNPRTUVWY";
-
-    switch( type )
-    {
-    default:
-    case NUMBERING_NUMERIC: return alphaNumeric;
-    case NUMBERING_HEX: return alphaHex;
-    case NUMBERING_ALPHA_NO_IOSQXZ: return alphaNoIOSQXZ;
-    case NUMBERING_ALPHA_FULL: return alphaFull;
-    }
-}
-
-
-bool ARRAY_OPTIONS::SchemeNonUnitColsStartAt0( NUMBERING_TYPE_T type )
-{
-    return type == NUMBERING_ALPHA_FULL || type == NUMBERING_ALPHA_NO_IOSQXZ;
-}
-
-
-bool ARRAY_OPTIONS::GetNumberingOffset(
-        const wxString& str, ARRAY_OPTIONS::NUMBERING_TYPE_T type, int& offsetToFill )
-{
-    const wxString& alphabet = ARRAY_OPTIONS::AlphabetFromNumberingScheme( type );
-
-    int       offset = 0;
-    const int radix = alphabet.length();
-
-    for( unsigned i = 0; i < str.length(); i++ )
-    {
-        int chIndex = alphabet.Find( str[i], false );
-
-        if( chIndex == wxNOT_FOUND )
-            return false;
-
-        const bool start0 = ARRAY_OPTIONS::SchemeNonUnitColsStartAt0( type );
-
-        // eg "AA" is actually index 27, not 26
-        if( start0 && i < str.length() - 1 )
-            chIndex++;
-
-        offset *= radix;
-        offset += chIndex;
-    }
-
-    offsetToFill = offset;
-    return true;
-}
-
-
-wxString ARRAY_OPTIONS::getCoordinateNumber( int n, NUMBERING_TYPE_T type )
-{
-    wxString        itemNum;
-    const wxString& alphabet = AlphabetFromNumberingScheme( type );
-
-    const bool nonUnitColsStartAt0 = SchemeNonUnitColsStartAt0( type );
-
-    bool firstRound = true;
-    int  radix = alphabet.Length();
-
-    do
-    {
-        int modN = n % radix;
-
-        if( nonUnitColsStartAt0 && !firstRound )
-            modN--; // Start the "tens/hundreds/etc column" at "Ax", not "Bx"
-
-        itemNum.insert( 0, 1, alphabet[modN] );
-
-        n /= radix;
-        firstRound = false;
-    } while( n );
-
-    return itemNum;
-}
-
 
 int ARRAY_GRID_OPTIONS::GetArraySize() const
 {
@@ -167,12 +87,12 @@ wxString ARRAY_GRID_OPTIONS::GetItemNumber( int n ) const
     {
         VECTOR2I coords = getGridCoords( n );
 
-        itemNum += getCoordinateNumber( coords.x + m_numberingOffsetX, m_priAxisNumType );
-        itemNum += getCoordinateNumber( coords.y + m_numberingOffsetY, m_secAxisNumType );
+        itemNum << m_pri_axis.GetItemNumber( coords.x );
+        itemNum << m_sec_axis.GetItemNumber( coords.y );
     }
     else
     {
-        itemNum += getCoordinateNumber( n + m_numberingOffsetX, m_priAxisNumType );
+        itemNum << m_pri_axis.GetItemNumber( n );
     }
 
     return itemNum;
@@ -209,5 +129,5 @@ ARRAY_OPTIONS::TRANSFORM ARRAY_CIRCULAR_OPTIONS::GetTransform( int n, const VECT
 
 wxString ARRAY_CIRCULAR_OPTIONS::GetItemNumber( int aN ) const
 {
-    return getCoordinateNumber( aN + m_numberingOffset, m_numberingType );
+    return m_axis.GetItemNumber( aN );
 }
diff --git a/include/array_axis.h b/include/array_axis.h
new file mode 100644
index 0000000000..49388b2f95
--- /dev/null
+++ b/include/array_axis.h
@@ -0,0 +1,110 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see AUTHORS.txt for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+#ifndef ARRAY_AXIS__H
+#define ARRAY_AXIS__H
+
+#include <core/optional.h>
+
+#include <wx/string.h>
+
+/**
+ * Class that contains information about a single array axis and the numbering
+ * of items along that axis.
+ *
+ * For example, a rectangular grid has two axes, X and Y, but a circular array
+ * has only one, that runs around the circle.
+ */
+class ARRAY_AXIS
+{
+public:
+    enum NUMBERING_TYPE
+    {
+        NUMBERING_NUMERIC = 0, ///< Arabic numerals: 0,1,2,3,4,5,6,7,8,9,10,11...
+        NUMBERING_HEX,
+        NUMBERING_ALPHA_NO_IOSQXZ, /*!< Alphabet, excluding IOSQXZ
+                                     *
+                                     * Per ASME Y14.35M-1997 sec. 5.2 (previously MIL-STD-100 sec. 406.5)
+                                     * as these can be confused with numerals and are often not used
+                                     * for pin numbering on BGAs, etc
+                                     */
+        NUMBERING_ALPHA_FULL,      ///< Full 26-character alphabet
+    };
+
+    ARRAY_AXIS();
+
+    /**
+     * Get the alphabet for the current numbering scheme.
+     * @param  type the numbering scheme
+     * @return      the alphabet (as a string)
+     */
+    const wxString& GetAlphabet() const;
+
+    /**
+     * Set the axis numbering type
+     */
+    void SetAxisType( NUMBERING_TYPE aType );
+
+    /**
+     * Set the axis start (as a string, which should decode to a valid index
+     * in the alphabet)
+     */
+    bool SetOffset( const wxString& aOffsetName );
+
+    /**
+     * Set the start offset for the series (e.g. 0 to start at 0/A, 4 to start
+     * at 4/E).
+     *
+     * @param aOffset offset of the first item in the
+     */
+    void SetOffset( int aOffset );
+
+    /**
+     * Get the numbering offset for the axis
+     *
+     * @return       the current offset
+     */
+    int GetOffset() const;
+
+    /**
+     * Get the position number (name) for the n'th axis point
+     *
+     * @param  n array point index, from 0
+     * @return   the point's name
+     */
+    wxString GetItemNumber( int n ) const;
+
+private:
+    /**
+     * Get the numbering offset for a given numbering string
+     *
+     * @param  str   a numbering string, say "B" or "5"
+     * @return       the offset, if found, else empty
+     */
+    OPT<int> getNumberingOffset( const wxString& str ) const;
+
+    NUMBERING_TYPE m_type;
+    int            m_offset;
+};
+
+#endif // ARRAY_AXIS__H
\ No newline at end of file
diff --git a/include/array_options.h b/include/array_options.h
index 8be7d5ccf5..fb758df0ec 100644
--- a/include/array_options.h
+++ b/include/array_options.h
@@ -26,6 +26,8 @@
 
 #include <math/vector2d.h>
 
+#include <array_axis.h>
+
 /**
  * Options that govern the setup of an "array" of multiple item.
  * The base #ARRAY_OPTIONS do not encode a specific geometry or numbering
@@ -40,20 +42,6 @@ public:
         ARRAY_CIRCULAR, ///< A circular array
     };
 
-    // NOTE: do not change order relative to charSetDescriptions
-    enum NUMBERING_TYPE_T
-    {
-        NUMBERING_NUMERIC = 0, ///< Arabic numerals: 0,1,2,3,4,5,6,7,8,9,10,11...
-        NUMBERING_HEX,
-        NUMBERING_ALPHA_NO_IOSQXZ, /*!< Alphabet, excluding IOSQXZ
-                                     *
-                                     * Per ASME Y14.35M-1997 sec. 5.2 (previously MIL-STD-100 sec. 406.5)
-                                     * as these can be confused with numerals and are often not used
-                                     * for pin numbering on BGAs, etc
-                                     */
-        NUMBERING_ALPHA_FULL,      ///< Full 26-character alphabet
-    };
-
     ARRAY_OPTIONS( ARRAY_TYPE_T aType )
             : m_type( aType ), m_shouldNumber( false ), m_numberingStartIsSpecified( false )
     {
@@ -61,29 +49,6 @@ public:
 
     virtual ~ARRAY_OPTIONS(){};
 
-    /**
-     * Get the alphabet for a particular numbering scheme.
-     * @param  type the numbering scheme
-     * @return      the alphabet (as a string)
-     */
-    static const wxString& AlphabetFromNumberingScheme( NUMBERING_TYPE_T type );
-
-    /**
-     * @return False for schemes like 0,1...9,10
-     *         True for schemes like A,B..Z,AA (where the tens column starts with char 0)
-     */
-    static bool SchemeNonUnitColsStartAt0( NUMBERING_TYPE_T type );
-
-    /**
-     * Get the numbering offset for a given numbering string
-     * @param  str   a numbering string, say "B" or "5"
-     * @param  type  the type this string should be
-     * @param  offsetToFill the offset to set, if found
-     * @return       true if the string is a valid offset of this type
-     */
-    static bool GetNumberingOffset(
-            const wxString& str, ARRAY_OPTIONS::NUMBERING_TYPE_T type, int& offsetToFill );
-
     /**
      * Transform applied to an object by this array
      */
@@ -143,7 +108,6 @@ public:
     }
 
 protected:
-    static wxString getCoordinateNumber( int n, NUMBERING_TYPE_T type );
 
     ARRAY_TYPE_T m_type;
 
@@ -166,11 +130,7 @@ struct ARRAY_GRID_OPTIONS : public ARRAY_OPTIONS
               m_reverseNumberingAlternate( false ),
               m_stagger( 0 ),
               m_stagger_rows( true ),
-              m_2dArrayNumbering( false ),
-              m_numberingOffsetX( 0 ),
-              m_numberingOffsetY( 0 ),
-              m_priAxisNumType( NUMBERING_NUMERIC ),
-              m_secAxisNumType( NUMBERING_NUMERIC )
+              m_2dArrayNumbering( false )
     {
     }
 
@@ -181,8 +141,7 @@ struct ARRAY_GRID_OPTIONS : public ARRAY_OPTIONS
     long             m_stagger;
     bool             m_stagger_rows;
     bool             m_2dArrayNumbering;
-    int              m_numberingOffsetX, m_numberingOffsetY;
-    NUMBERING_TYPE_T m_priAxisNumType, m_secAxisNumType;
+    ARRAY_AXIS       m_pri_axis, m_sec_axis;
 
     TRANSFORM GetTransform( int aN, const VECTOR2I& aPos ) const override;
     int       GetArraySize() const override;
@@ -199,9 +158,7 @@ struct ARRAY_CIRCULAR_OPTIONS : public ARRAY_OPTIONS
             : ARRAY_OPTIONS( ARRAY_CIRCULAR ),
               m_nPts( 0 ),
               m_angle( 0.0f ),
-              m_rotateItems( false ),
-              m_numberingType( NUMBERING_NUMERIC ),
-              m_numberingOffset( 0 )
+              m_rotateItems( false )
     {
     }
 
@@ -211,8 +168,7 @@ struct ARRAY_CIRCULAR_OPTIONS : public ARRAY_OPTIONS
     double           m_angle;
     VECTOR2I         m_centre;
     bool             m_rotateItems;
-    NUMBERING_TYPE_T m_numberingType;
-    long             m_numberingOffset;
+    ARRAY_AXIS       m_axis;
 
     TRANSFORM GetTransform( int aN, const VECTOR2I& aPos ) const override;
     int       GetArraySize() const override;
diff --git a/pcbnew/dialogs/dialog_create_array.cpp b/pcbnew/dialogs/dialog_create_array.cpp
index f96e5b2162..ed9b0b3fb7 100644
--- a/pcbnew/dialogs/dialog_create_array.cpp
+++ b/pcbnew/dialogs/dialog_create_array.cpp
@@ -114,7 +114,7 @@ DIALOG_CREATE_ARRAY::DIALOG_CREATE_ARRAY(
     // Set up numbering scheme drop downs
     //
     // character set
-    // NOTE: do not change the order of this relative to the NUMBERING_TYPE_T enum
+    // NOTE: do not change the order of this relative to the NUMBERING_TYPE enum
     const wxString charSetDescriptions[] =
     {
         _( "Numerals (0,1,2,...,9,10)" ),
@@ -206,17 +206,15 @@ void DIALOG_CREATE_ARRAY::OnParameterChanged( wxCommandEvent& event )
  * @return if all valid
  */
 static bool validateNumberingTypeAndOffset( const wxTextCtrl& offsetEntry,
-                                            const wxChoice& typeEntry,
-                                            ARRAY_OPTIONS::NUMBERING_TYPE_T& type,
-                                            int& offset, wxArrayString& errors )
+        const wxChoice& typeEntry, ARRAY_AXIS& aAxis, wxArrayString& errors )
 {
     const int typeVal = typeEntry.GetSelection();
     // mind undefined casts to enums (should not be able to happen)
-    bool ok = typeVal <= ARRAY_OPTIONS::NUMBERING_TYPE_MAX;
+    bool ok = typeVal <= ARRAY_AXIS::NUMBERING_TYPE_MAX;
 
     if( ok )
     {
-        type = (ARRAY_OPTIONS::NUMBERING_TYPE_T) typeVal;
+        aAxis.SetAxisType( static_cast<ARRAY_AXIS::NUMBERING_TYPE>( typeVal ) );
     }
     else
     {
@@ -228,17 +226,19 @@ static bool validateNumberingTypeAndOffset( const wxTextCtrl& offsetEntry,
     }
 
     const wxString text = offsetEntry.GetValue();
-    ok = ARRAY_OPTIONS::GetNumberingOffset( text, type, offset );
+
+    ok = aAxis.SetOffset( text );
 
     if( !ok )
     {
-        const wxString& alphabet = ARRAY_OPTIONS::AlphabetFromNumberingScheme( type );
+        const wxString& alphabet = aAxis.GetAlphabet();
 
         wxString err;
         err.Printf( _( "Could not determine numbering start from \"%s\": "
                        "expected value consistent with alphabet \"%s\"" ),
                     text, alphabet );
         errors.Add(err);
+        return false;
     }
 
     return ok;
@@ -311,14 +311,12 @@ bool DIALOG_CREATE_ARRAY::TransferDataFromWindow()
 
                 // validate from the input fields
                 bool numOk = validateNumberingTypeAndOffset( *m_entryGridPriNumberingOffset,
-                        *m_choicePriAxisNumbering, newGrid->m_priAxisNumType,
-                        newGrid->m_numberingOffsetX, errors );
+                        *m_choicePriAxisNumbering, newGrid->m_pri_axis, errors );
 
                 if( newGrid->m_2dArrayNumbering )
                 {
                     numOk = validateNumberingTypeAndOffset( *m_entryGridSecNumberingOffset,
-                                    *m_choiceSecAxisNumbering, newGrid->m_secAxisNumType,
-                                    newGrid->m_numberingOffsetY, errors )
+                                    *m_choiceSecAxisNumbering, newGrid->m_sec_axis, errors )
                             && numOk;
                 }
 
@@ -328,8 +326,8 @@ bool DIALOG_CREATE_ARRAY::TransferDataFromWindow()
             {
                 // artificial linear numeric scheme from 1
                 newGrid->m_2dArrayNumbering = false;
-                newGrid->m_priAxisNumType = ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC;
-                newGrid->m_numberingOffsetX = 1; // Start at "1"
+                newGrid->m_pri_axis.SetAxisType( ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC );
+                newGrid->m_pri_axis.SetOffset( 1 );
             }
         }
 
@@ -359,16 +357,23 @@ bool DIALOG_CREATE_ARRAY::TransferDataFromWindow()
 
             if( newCirc->GetNumberingStartIsSpecified() )
             {
-                newCirc->m_numberingType = ARRAY_OPTIONS::NUMBERING_NUMERIC;
+                newCirc->m_axis.SetAxisType( ARRAY_AXIS::NUMBERING_NUMERIC );
+
+                long offset;
 
                 ok = ok
-                     && validateLongEntry( *m_entryCircNumberingStart, newCirc->m_numberingOffset,
-                                _( "numbering start" ), errors );
+                     && validateLongEntry(
+                             *m_entryCircNumberingStart, offset, _( "numbering start" ), errors );
+
+                if( ok )
+                {
+                    newCirc->m_axis.SetOffset( offset );
+                }
             }
             else
             {
                 // artificial linear numeric scheme from 1
-                newCirc->m_numberingOffset = 1; // Start at "1"
+                newCirc->m_axis.SetOffset( 1 ); // Start at "1"
             }
         }
 
diff --git a/qa/common/CMakeLists.txt b/qa/common/CMakeLists.txt
index dc9ac1d1c4..5f8e4b67c9 100644
--- a/qa/common/CMakeLists.txt
+++ b/qa/common/CMakeLists.txt
@@ -36,6 +36,7 @@ set( common_srcs
     ../../common/colors.cpp
     ../../common/observable.cpp
 
+    test_array_axis.cpp
     test_array_options.cpp
     test_color4d.cpp
     test_coroutine.cpp
diff --git a/qa/common/test_array_axis.cpp b/qa/common/test_array_axis.cpp
new file mode 100644
index 0000000000..baf5b62174
--- /dev/null
+++ b/qa/common/test_array_axis.cpp
@@ -0,0 +1,209 @@
+/*
+ * This program source code file is part of KiCad, a free EDA CAD application.
+ *
+ * Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, you may find one here:
+ * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+ * or you may search the http://www.gnu.org website for the version 2 license,
+ * or you may write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
+ */
+
+/**
+ * @file
+ * Test suite for #ARRAY_AXIS
+ */
+
+#include <unit_test_utils/unit_test_utils.h>
+
+#include <array_axis.h>
+
+/**
+ * Declare the test suite
+ */
+BOOST_AUTO_TEST_SUITE( ArrayAxis )
+
+struct VALID_OFFSET_CASE
+{
+    ARRAY_AXIS::NUMBERING_TYPE m_axis_type;
+    std::string                m_offset_str;
+    bool                       m_exp_valid;
+    int                        m_exp_offset;
+};
+
+
+/**
+ * Check we can get valid (or invalid) offsets as expected
+ */
+BOOST_AUTO_TEST_CASE( ValidOffsets )
+{
+    // clang-format off
+    const std::vector<VALID_OFFSET_CASE> cases = {
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            "0",
+            true,
+            0,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            "1",
+            true,
+            1,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            "1234",
+            true,
+            1234,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            "",
+            false,
+            0,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            "www",
+            false,
+            0,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
+            "A",
+            true,
+            0,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
+            "XY",
+            true,
+            648,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_HEX,
+            "A0",
+            true,
+            160,
+        },
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_HEX,
+            "G0",
+            false,
+            0,
+        },
+    };
+    // clang-format on
+
+    for( const auto& c : cases )
+    {
+        ARRAY_AXIS axis;
+        axis.SetAxisType( c.m_axis_type );
+
+        bool offset_ok = axis.SetOffset( c.m_offset_str );
+
+        BOOST_CHECK_EQUAL( offset_ok, c.m_exp_valid );
+
+        if( c.m_exp_valid )
+        {
+            BOOST_CHECK_EQUAL( axis.GetOffset(), c.m_exp_offset );
+        }
+    }
+}
+
+/**
+ * Data for testing a single array axis
+ */
+struct ARRAY_AXIS_NAMING_PARAMS
+{
+    ARRAY_AXIS::NUMBERING_TYPE m_axis_type;
+    std::string                m_start_at;
+};
+
+struct ARRAY_AXIS_NAMING_CASE
+{
+    std::string              m_case_name;
+    ARRAY_AXIS_NAMING_PARAMS m_prms;
+    int                      m_num;
+    std::vector<std::string> m_exp_names;
+};
+
+
+// clang-format off
+static const std::vector<ARRAY_AXIS_NAMING_CASE> axis_name_cases = {
+    {
+        "Linear",
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            "1",
+        },
+        6,
+        { "1", "2", "3", "4", "5", "6" },
+    },
+    {
+        // Test alphabetical
+        "Alpha",
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
+            "A",
+        },
+        3,
+        { "A", "B", "C" },
+    },
+    {
+        // Test alphabetical with 2nd col
+        "Alpha 2nd col",
+        {
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
+            "Y",
+        },
+        4,
+        { "Y", "Z", "AA", "AB" },
+    },
+};
+// clang-format on
+
+/**
+ * Test of the naming cases
+ */
+BOOST_AUTO_TEST_CASE( Numbering )
+{
+    for( const auto& c : axis_name_cases )
+    {
+        BOOST_TEST_CONTEXT( c.m_case_name )
+        {
+            ARRAY_AXIS axis;
+            axis.SetAxisType( c.m_prms.m_axis_type );
+
+            bool start_ok = axis.SetOffset( c.m_prms.m_start_at );
+
+            // All these examples have valid start offsets
+            BOOST_CHECK( start_ok );
+
+            std::vector<std::string> names;
+
+            for( int i = 0; i < c.m_num; i++ )
+            {
+                names.push_back( axis.GetItemNumber( i ).ToStdString() );
+            }
+
+            BOOST_CHECK_EQUAL_COLLECTIONS(
+                    names.begin(), names.end(), c.m_exp_names.begin(), c.m_exp_names.end() );
+        }
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
\ No newline at end of file
diff --git a/qa/common/test_array_options.cpp b/qa/common/test_array_options.cpp
index cfb510d554..b72cf76f32 100644
--- a/qa/common/test_array_options.cpp
+++ b/qa/common/test_array_options.cpp
@@ -392,14 +392,14 @@ void CheckArrayNumbering( const ARRAY_OPTIONS& aOpts, const std::vector<std::str
 
 struct GRID_ARRAY_NAMING_PARAMS
 {
-    ARRAY_OPTIONS::NUMBERING_TYPE_T m_pri_type;
-    ARRAY_OPTIONS::NUMBERING_TYPE_T m_sec_type;
-    std::string                     m_start_at_x;
-    std::string                     m_start_at_y;
-    bool                            m_2d_numbering;
-    bool                            m_h_then_v;
-    int                             m_nx;
-    int                             m_ny;
+    ARRAY_AXIS::NUMBERING_TYPE m_pri_type;
+    ARRAY_AXIS::NUMBERING_TYPE m_sec_type;
+    std::string                m_start_at_x;
+    std::string                m_start_at_y;
+    bool                       m_2d_numbering;
+    bool                       m_h_then_v;
+    int                        m_nx;
+    int                        m_ny;
 };
 
 
@@ -416,8 +416,8 @@ static const std::vector<GRID_ARRAY_NAMING_CASE> grid_name_cases = {
     {
         "Linear grid",
         {
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC,
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC, // doesn't matter here
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC, // doesn't matter here
             "1",
             "2",
             false,
@@ -431,8 +431,8 @@ static const std::vector<GRID_ARRAY_NAMING_CASE> grid_name_cases = {
         // Tests a 2d grid
         "2D grid A1",
         {
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_ALPHA_FULL,
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
             "A",
             "1",
             true,
@@ -446,8 +446,8 @@ static const std::vector<GRID_ARRAY_NAMING_CASE> grid_name_cases = {
         // Tests a 2d grid
         "2D grid 11",
         {
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC,
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
             "1",
             "1",
             true,
@@ -463,8 +463,8 @@ static const std::vector<GRID_ARRAY_NAMING_CASE> grid_name_cases = {
         // Tests a 2d grid, with different types and offsets (and alphabet wrap)
         "2D grid offsets",
         {
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC,
-            ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_ALPHA_FULL,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC,
+            ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_ALPHA_FULL,
             "5",
             "Z",
             true,
@@ -494,13 +494,11 @@ BOOST_AUTO_TEST_CASE( GridNaming )
 
             grid_opts.m_horizontalThenVertical = c.m_prms.m_h_then_v;
 
-            ARRAY_OPTIONS::GetNumberingOffset(
-                    c.m_prms.m_start_at_x, c.m_prms.m_pri_type, grid_opts.m_numberingOffsetX );
-            ARRAY_OPTIONS::GetNumberingOffset(
-                    c.m_prms.m_start_at_y, c.m_prms.m_sec_type, grid_opts.m_numberingOffsetY );
+            grid_opts.m_pri_axis.SetAxisType( c.m_prms.m_pri_type );
+            grid_opts.m_sec_axis.SetAxisType( c.m_prms.m_sec_type );
 
-            grid_opts.m_priAxisNumType = c.m_prms.m_pri_type;
-            grid_opts.m_secAxisNumType = c.m_prms.m_sec_type;
+            grid_opts.m_pri_axis.SetOffset( c.m_prms.m_start_at_x );
+            grid_opts.m_sec_axis.SetOffset( c.m_prms.m_start_at_y );
 
             grid_opts.m_2dArrayNumbering = c.m_prms.m_2d_numbering;
 
diff --git a/qa/pcbnew/test_array_pad_name_provider.cpp b/qa/pcbnew/test_array_pad_name_provider.cpp
index 04f4959b78..748b5613ac 100644
--- a/qa/pcbnew/test_array_pad_name_provider.cpp
+++ b/qa/pcbnew/test_array_pad_name_provider.cpp
@@ -82,8 +82,8 @@ std::vector<APNP_CASE> GetModuleAPNPCases()
 
     // simple linear numbering
     opts->m_2dArrayNumbering = false;
-    opts->m_numberingOffsetX = 1;
-    opts->m_priAxisNumType = ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC;
+    opts->m_pri_axis.SetOffset( 1 );
+    opts->m_pri_axis.SetAxisType( ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC );
 
     cases.push_back( {
             "Simple linear, skip some",
@@ -98,8 +98,8 @@ std::vector<APNP_CASE> GetModuleAPNPCases()
 
     // simple linear numbering (again)
     opts->m_2dArrayNumbering = false;
-    opts->m_numberingOffsetX = 1;
-    opts->m_priAxisNumType = ARRAY_OPTIONS::NUMBERING_TYPE_T::NUMBERING_NUMERIC;
+    opts->m_pri_axis.SetOffset( 1 );
+    opts->m_pri_axis.SetAxisType( ARRAY_AXIS::NUMBERING_TYPE::NUMBERING_NUMERIC );
 
     cases.push_back( {
             "Simple linear, no module",